/**
 * HttpEngine.java
 *
 * @author tianli
 * 
 * @date 2011-3-3
 * 
 */
package com.baidu.common.net;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;

import com.baidu.common.utils.AppLog;
import com.baidu.common.utils.DeviceInfo;
import com.baidu.common.utils.Tools;
import com.japher.hackson.AppContext;

/**
 * @author tianli
 * 
 */
public class HttpEngine {
    
    private static final String TAG = HttpEngine.class.getName();
    private static final int TIMEOUT = 10000; 
    
    private HashMap<String, String> headers = new HashMap<String, String>();
    private HttpClient httpClient;
    
    private long length = -1;
    private int timeoutMs = TIMEOUT;
    
    public static HttpEngine create() {
        return new HttpEngine();
    }
    
    private HttpEngine() {
        httpClient = createHttpClient();
        // connectionManager = httpClient.getConnectionManager();
    }
    
    /**
     *   @param ms
     *            set timeout to ms
     */
    public void setTimoutMs(int ms) {
        timeoutMs = ms;
        httpClient.getParams().setIntParameter("http.socket.timeout", timeoutMs);
    }
    
    /**
     * @param request
     *            send request
     * @return return the response from server if got any. if any network error
     *         happens,return null
     * @throws NetworkTimeoutException
     * @throws NetworkErrorException
     * @throws ServerErrorException
     */
    public InputStream executeHttpRequest(HttpRequestData request) throws NetworkTimeoutException,
        NetworkErrorException, ServerErrorException {
        HttpResponse response = null;
        HttpRequestBase httpRequest = null;
        InputStream is = null;
        
        //boolean isHttps;
        final String url = request.getUrl();
        if (url == null || url.trim().length() == 0)
            return null;
        
        try {
            //isHttps = request.getUrl().startsWith("https");
            // DeviceInfo deviceInfo = DeviceInfo.getInstance();
            // if(!deviceInfo.isWapApn() || !isHttps)
            {
                // HttpPost httpPost
                if (request.isGet()) {
                    httpRequest = new HttpGet(url);
                } else {
                    httpRequest = new HttpPost(url);
                }
                
                if (request.isAppUrl()) {
                    if (HttpRequestData.userAgent != null) {
                        httpRequest.setHeader("User-Agent", HttpRequestData.userAgent);
                    }
                    
                    if (request.isGzip()) {
                        httpRequest.setHeader("Accept-Encoding", "gzip");
                    }
                    
                    if (request.getCookie() != null) {
                        httpRequest.setHeader("Cookie", request.getCookie());
                    }
                }
                
                if (!request.isGet() && request.getData() != null) {
                    ByteArrayEntity byteEntity = null;
                    
                    if (request.getFile() != null) {
                        ByteArrayOutputStream output = new ByteArrayOutputStream();
                        FileInputStream stream = null;
                        try {
                            
                            output.write(request.getData());
                            stream = new FileInputStream(request.getFile());
                            output.write("&headPhoto=".getBytes());
                            byte data[] = new byte[1024];
                            
                            int len = 1024;
                            while (len != -1 && len == 1024) {
                                len = stream.read(data);
                                output.write(data, 0, len);
                            }
                            httpRequest.setHeader("Content-Type",
                                "application/x-www-form-urlencoded");
                            byteEntity = new ByteArrayEntity(output.toByteArray());
                            ((HttpPost) httpRequest).setEntity(byteEntity);
                        } catch (Exception e) {
                            
                        } finally {
                            try {
                                
                            } catch (Exception e) {
                                output.close();
                                stream.close();
                            }
                        }
                        
                    } else {
                        httpRequest.setHeader("Content-Type", "application/x-www-form-urlencoded");
                        byteEntity = new ByteArrayEntity(request.getData());
                        ((HttpPost) httpRequest).setEntity(byteEntity);
                    }
                    // byteEntity.setContentType("application/x-www-form-urlencoded");
                }
                // httpRequest.setHeader("Connection", "Keep-Alive");
            }
            /*
             * else { Server proxyServer = deviceInfo.getApn().getProxyServer();
             * httpsConn = createHttpsConnection(request.getUrl(),
             * request.getData(), proxyServer);
             * 
             * }
             */
            
            // add HTTP headers
            Iterator<Entry<String, String>> it = headers.entrySet().iterator();
            while (it.hasNext()) {
                Entry<String, String> entry = (Entry<String, String>) it.next();
                // if(!deviceInfo.isWapApn() || !isHttps)
                {
                    httpRequest.addHeader((String) entry.getKey(), (String) entry.getValue());
                }
                /*
                 * else {
                 * httpsConn.addRequestProperty((String)entry.getKey(),(String
                 * )entry.getValue()); }
                 */
            }
            
            int statusCode;
            // if(!deviceInfo.isWapApn() || !isHttps)
            {
                response = httpClient.execute(httpRequest);
                statusCode = response.getStatusLine().getStatusCode();
            }
            /*
             * else { statusCode = httpsConn.getResponseCode(); }
             */
            
            switch (statusCode) {
                case 200:
                    if (response != null) {
                        is = response.getEntity().getContent();
                        length = response.getEntity().getContentLength();
                        
//                        Header h2[] = response.getHeaders("Set-Cookie");
//                        for (int i = 0; i < h2.length; i++) {
//                            String v = h2[i].getValue();
//                            if (v.startsWith("LDHUI_SESS")) {
//                                request.setCookie(v);
//                                break;
//                            }
//                        }
                    }
                    return is;
                default:
                    AppLog.debug(TAG, "Got response: " + "HTTP Code: " + statusCode);
                    
                    String errorJson = "";
                    if (response != null) {
                        ByteArrayOutputStream out = new ByteArrayOutputStream();
                        byte data[] = null;
                        int c = 0;
                        try {
                            try {
                                is = response.getEntity().getContent();
                            } catch (Exception e) {
                            }
                            while ((c = is.read()) != -1) {
                                out.write(c);
                            }
                            
                            data = out.toByteArray();
                            errorJson = new String(data);
                            AppLog.info(TAG, "Error Description: " + errorJson);
                        } catch (Exception e) {
                            
                        }
                        AppLog.info(TAG, "StatusLine: " + response.getStatusLine().toString());
                        response.getEntity().consumeContent();
                    }
                    if (Tools.isEmpty(errorJson)) {
                        throw new ServerErrorException("network error with error code "
                            + statusCode);
                    } else {
                        throw new ServerErrorException("network error with error code "
                            + statusCode).setErrordJson(errorJson);
                    }
            }
        } catch (ConnectTimeoutException e) {
            throw new NetworkTimeoutException(e.getMessage() + "\n" + Tools.getStackTrace(e));
        } catch (SocketTimeoutException e) {
            throw new NetworkTimeoutException(e.getMessage() + "\n" + Tools.getStackTrace(e));
        } catch (ServerErrorException e) {
            throw e;
        } catch (Exception e) {
            throw new NetworkErrorException(e.getMessage() + "\n" + Tools.getStackTrace(e));
        }
    }
    
    @SuppressWarnings("unused")
    private static final HttpsURLConnection createHttpsConnection(String url, byte[] data,
        Server proxyServer) {
        HttpsURLConnection httpsConn = null;
        try {
            if (url != null && url.length() > 0) {
                if (url.startsWith("https")) {
                    URL httpsUrl = new URL(url);
                    if (proxyServer != null && !Tools.isEmpty(proxyServer.getAddress())) {
                        int port = 80;
                        
                        port = proxyServer.getPort();
                        
                        Proxy proxy = new Proxy(Type.HTTP, new InetSocketAddress(
                            proxyServer.getAddress(), port));
                        httpsConn = (HttpsURLConnection) httpsUrl.openConnection(proxy);
                    } else {
                        httpsConn = (HttpsURLConnection) httpsUrl.openConnection();
                    }
                    httpsConn.setDoOutput(true);
                    httpsConn.setUseCaches(false);
                    if (HttpRequestData.userAgent != null)
                        httpsConn.addRequestProperty("User-Agent", HttpRequestData.userAgent);
                    httpsConn.setReadTimeout(TIMEOUT * 2);
                    httpsConn.setConnectTimeout(TIMEOUT * 2);
                    SSLContext sslContext = SSLContext.getInstance("SSL");
                    TrustManager tm = new MyX509TrustManager();
                    sslContext.init(null, new TrustManager[] { tm },
                        new java.security.SecureRandom());
                    httpsConn.setSSLSocketFactory(sslContext.getSocketFactory());
                    httpsConn.setHostnameVerifier(new MyHostnameVerifier());
                    if (data != null && data.length > 0) {
                        httpsConn.setRequestMethod("POST");
                        httpsConn.setRequestProperty("Content-Type",
                            "application/x-www-form-urlencoded;charset=UTF-8");
                        httpsConn.getOutputStream().write(data);
                        httpsConn.getOutputStream().flush();
                        httpsConn.getOutputStream().close();
                    }
                }
            }
        } catch (Exception e) {
        }
        return httpsConn;
    }
    
    /**
     * Create the default HTTP protocol parameters.
     */
    protected static final HttpParams createHttpParams() {
        
        final HttpParams params = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(params, TIMEOUT);
        HttpConnectionParams.setSoTimeout(params, TIMEOUT * 4); // socket
                                                                // timeout is
                                                                // 20s
        DeviceInfo deviceInfo = DeviceInfo.getInstance();
        if (deviceInfo.isWapApn(AppContext.getInstance().getContext())) {
            Server proxyServer = deviceInfo.getApn(AppContext.getInstance().getContext())
                .getProxyServer();
            // use apn proxy
            if (!Tools.isEmpty(proxyServer.getAddress())) {
                int port = 80;
                port = proxyServer.getPort();
                HttpHost proxy = new HttpHost(proxyServer.getAddress(), port, "http");
                params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
            }
        }
        return params;
    }
    
    public static final DefaultHttpClient createHttpClient() {
        
        final SchemeRegistry supportedSchemes = new SchemeRegistry();
        SSLSocketFactory socketFactory = null;
        // SSL
        try {
            KeyStore trustStore;
            trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            trustStore.load(null, null);
            socketFactory = new MySSLSocketFactory(trustStore);
        } catch (Exception e) {
            AppLog.error(TAG, e.getMessage(), e);
        }
        
        if (socketFactory == null) {
            socketFactory = SSLSocketFactory.getSocketFactory();
        }
        
        X509HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.STRICT_HOSTNAME_VERIFIER;
        socketFactory.setHostnameVerifier((X509HostnameVerifier) hostnameVerifier);
        supportedSchemes.register(new Scheme("https", socketFactory, 443));
        
        HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
        final SocketFactory sf = PlainSocketFactory.getSocketFactory();
        supportedSchemes.register(new Scheme("http", sf, 80));
        // supportedSchemes.register(new Scheme("http", sf, 8080));
        // Set some client http client parameter defaults.
        final HttpParams httpParams = createHttpParams();
        HttpClientParams.setRedirecting(httpParams, false);
        final ClientConnectionManager ccm = new ThreadSafeClientConnManager(httpParams,supportedSchemes);
        return new DefaultHttpClient(ccm, httpParams);
    }
    
    private static class MyHostnameVerifier implements HostnameVerifier {
        
        public boolean verify(String hostname, SSLSession session) {
            // Open SSLSocket directly to gmail.com
            //HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
            
            // Verify that the certicate hostname is for mail.google.com
            // This is due to lack of SNI support in the current SSLSocket.
            // if (!hv.verify("piao.163.com", session))
            // {
            // return false;
            // }
            
            // At this point SSLSocket performed certificate verificaiton and
            // we have performed hostname verification, so it is safe to
            // proceed.
            
            // ... use socket ...
            // socket.close();
            
            return true;
        }
    }
    
    private static class MyX509TrustManager implements X509TrustManager {
        public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate,
            String paramString) throws CertificateException {
        }
        
        public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate,
            String paramString) throws CertificateException {
        }
        
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    }
    
    public long getLength() {
        return length;
    }
    
    public void addHeader(String key, String value) {
        headers.put(key, value);
    }
    
    public void reset() {
        headers.clear();
    }
    
    
    private static class MySSLSocketFactory extends SSLSocketFactory {
        SSLContext sslContext = SSLContext.getInstance("TLS");
        
        public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException,
            KeyManagementException, KeyStoreException, UnrecoverableKeyException {
            super(truststore);
            TrustManager tm = new MyX509TrustManager();
            sslContext.init(null, new TrustManager[] { tm }, null);
        }
        
        @Override
        public Socket createSocket(Socket socket, String host, int port, boolean autoClose)
            throws IOException, UnknownHostException {
            if (port == -1)
                port = 443;
            return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
        }
        
        @Override
        public Socket createSocket() throws IOException {
            return sslContext.getSocketFactory().createSocket();
        }
        
//        @Override
//        public Socket connectSocket(Socket sock, String host, int port, InetAddress localAddress,
//            int localPort, HttpParams params) throws IOException {
//            AppLog.error(TAG, "connectSocket(Socket sock, String host, int port");
//            int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
//            int soTimeout = HttpConnectionParams.getSoTimeout(params);
//            InetSocketAddress remoteAddress = new InetSocketAddress(host, port);
//            SSLSocket sslsock = (SSLSocket) ((sock != null) ? sock : createSocket());
//            if ((localAddress != null) || (localPort > 0)) {
//                // we need to bind explicitly
//                if (localPort < 0) {
//                    localPort = 0; // indicates "any"
//                }
//                InetSocketAddress isa = new InetSocketAddress(localAddress, localPort);
//                sslsock.bind(isa);
//            }
//            sslsock.connect(remoteAddress, connTimeout);
//            sslsock.setSoTimeout(soTimeout);
//            return sslsock;
//        }
        
        @Override
        public boolean isSecure(Socket sock) throws IllegalArgumentException {
            return true;
        }
    }
    
}
